package baidu_netdisk

import (
	"context"
	"crypto/md5"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/chicken-team-outside/chicken_transmission/driver"
	bdclient "github.com/chicken-team-outside/chicken_transmission/driver/baidu/openxpanapi"
	"io"
	"strings"
	"time"
)

const BaiduNetDiskAppKey = "Gk1rBoQFFT79Os92W4dxNGukUx5MtQ50"
const BaiduNetDiskSecretKey = "hHUxop1HoNQZVK8W5ast6RZ6Q2X3ZbRF"
const SliceMd5Size = 256 * 1024

type PcsSuperFile2Rsp struct {
	Md5       string `json:"md5"`
	RequestId uint64 `json:"request_id"`
}

type BaiduAuthorizationData struct {
	ExpiresIn    int64  `json:"expires_in,omitempty"`
	RefreshToken string `json:"refresh_token,omitempty"`
	AccessToken  string `json:"access_token,omitempty"`
	CreateAt     int64  `json:"create_at,omitempty"`
}

func (b *BaiduAuthorizationData) String() string {
	return fmt.Sprintf("BaiduAuthorizationData{ExpiresIn=%d, RefreshToken=%s, AccessToken=%s, CreateAt=%d}", b.ExpiresIn, b.RefreshToken, b.AccessToken, b.CreateAt)
}

func RefreshAccessToken(ctx context.Context, refreshToken string) (*BaiduAuthorizationData, error) {
	execute, _, err := getApiClient().AuthApi.OauthTokenRefreshToken(ctx).
		RefreshToken(refreshToken).
		ClientId(BaiduNetDiskAppKey).
		ClientSecret(BaiduNetDiskSecretKey).Execute()
	if err != nil {
		return nil, err
	}
	return &BaiduAuthorizationData{
		RefreshToken: execute.GetRefreshToken(),
		AccessToken:  execute.GetAccessToken(),
		ExpiresIn:    int64(execute.GetExpiresIn()),
		CreateAt:     time.Now().Unix(),
	}, nil
}

type GenericOpenAPIErrorBody struct {
	Err              string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

func (e GenericOpenAPIErrorBody) Error() string {
	return fmt.Sprintf("%s: %s", e.Err, e.ErrorDescription)
}

func convertToGenericOpenAPIErrorBody(err error) *GenericOpenAPIErrorBody {
	if apiError, ok := err.(bdclient.GenericOpenAPIError); ok {
		r := &GenericOpenAPIErrorBody{}
		err01 := json.Unmarshal(apiError.Body(), r)
		if err01 != nil {
			return nil
		} else {
			return r
		}
	}
	return nil
}

// IsQrCodeExpiredOrRevoked
//
//	This function can check if the error returned by `CheckQrLoginResult` is whether the login QR code is expired or not.
func IsQrCodeExpiredOrRevoked(err error) bool {
	if r, ok := err.(*GenericOpenAPIErrorBody); ok {
		return r.Err == "invalid_grant"
	}
	return false
}

// CheckQrLoginResult
//
//	Get the result of user's authorization to scan the code.
//	@param deviceCode: The fields corresponding to the return value of the CreateQrLoginUrl function.
//	@return: If the user does not scan the code, the result and error will both be nil.
func CheckQrLoginResult(ctx context.Context, deviceCode string) (*BaiduAuthorizationData, error) {
	execute, _, err := getApiClient().AuthApi.OauthTokenDeviceTokenExecute(getApiClient().AuthApi.OauthTokenDeviceToken(ctx).ClientId(BaiduNetDiskAppKey).Code(deviceCode).ClientSecret(BaiduNetDiskSecretKey))
	if err != nil {
		body := convertToGenericOpenAPIErrorBody(err)
		if body != nil {
			if body.Err == "authorization_pending" {
				return nil, nil
			} else if body.Err == "slow_down" {
				select {
				case <-time.After(5 * time.Second):
				case <-ctx.Done():
					return nil, ctx.Err()
				}
				return nil, nil
			} else {
				return nil, body
			}
		}
		return nil, err
	}
	return &BaiduAuthorizationData{
		RefreshToken: execute.GetRefreshToken(),
		AccessToken:  execute.GetAccessToken(),
		ExpiresIn:    int64(execute.GetExpiresIn()),
		CreateAt:     time.Now().Unix(),
	}, nil
}

// countBlockMD5List
//
// return block_md5_list, slice_md5, content_md5, error
func countBlockMD5List(ctx context.Context, size int64, file io.ReadSeeker, blockSize int) ([]string, string, string, error) {
	blockList := make([]string, 0, size/int64(blockSize)+1)
	block := make([]byte, blockSize)
	_, err := file.Seek(0, io.SeekStart)
	if err != nil {
		return nil, "", "", err
	}
	var sliceMd5 string
	if size >= SliceMd5Size {
		buf := make([]byte, SliceMd5Size)
		_, err = io.ReadFull(file, buf)
		if err != nil {
			return nil, "", "", err
		}
		sliceMd5 = fmt.Sprintf("%x", md5.Sum(buf))
	}
	_, err = file.Seek(0, io.SeekStart)
	if err != nil {
		return nil, "", "", err
	}
	contentMd5 := md5.New()
	for ctx.Err() == nil {
		n, err := io.ReadFull(file, block)
		if err != nil {
			if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
				if n != 0 {
					contentMd5.Write(block[:n])
					blockList = append(blockList, fmt.Sprintf("%x", md5.Sum(block[:n])))
				}
				return blockList, sliceMd5, fmt.Sprintf("%x", contentMd5.Sum(nil)), nil
			}
		} else {
			contentMd5.Write(block)
			blockList = append(blockList, fmt.Sprintf("%x", md5.Sum(block)))
		}
	}
	return nil, "", "", ctx.Err()
}

func getFileBlock(file io.ReadSeeker, i, blockSize int, buf []byte) (int, error) {
	_, err := file.Seek(int64(i)*int64(blockSize), io.SeekStart)
	if err != nil {
		return 0, err
	}
	n, err := io.ReadFull(file, buf)
	if err != nil {
		if !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
			return 0, err
		}
	}
	return n, nil
}

// CreateQrLoginUrl
//
//	Create a QR login url.
//	This function returns the creation result.
func CreateQrLoginUrl(ctx context.Context) (*bdclient.OauthTokenDeviceCodeResponse, error) {
	execute, _, err := getApiClient().AuthApi.OauthTokenDeviceCodeExecute(getApiClient().AuthApi.
		OauthTokenDeviceCode(ctx).
		ClientId(BaiduNetDiskAppKey).
		Scope("basic,netdisk"))
	if err != nil {
		return nil, err
	}
	return &execute, nil
}

func formatPath(path string) string {
	return strings.Replace(path, "\\", "/", -1)
}

type getErrorCode interface {
	GetErrno() int32
}

func checkErrorCode(rsp getErrorCode) error {
	switch rsp.GetErrno() {
	case 0:
		return nil
	case -6:
		return driver.ErrAuthExpired
	case 31034:
		return driver.ErrHitFrequencyControl
	case -10:
		return driver.ErrExceedSpaceQuota
	default:
		return fmt.Errorf("unexpected error code: %d", rsp.GetErrno())
	}
}
